Android 安装包 APK 是如何生成的
这是 dhl 的第 25 篇原创文章
在 Android Studio 中直接点击 Run ‘app’ 就可以在 build/outputs/apk 生成可以在 android 设备中安装的 APK 文件,那么 APK 生成的过程是怎么样的呢?
APK 文件大概可以分为两个部分:代码和资源,所以打包的也分为代码和资源两个部分,我们可以根据 Google提供的流程图 来具体了解一个 APK 的构建过程
新版构建流程图
APK 打包的内容主要有:应用模块也就是自己开发的用到的源代码、资源文件、aidl 接口文件,还有就是依赖模块即源代码用到的第三方依赖库如:aar、jar、so 文件
为了能够清楚的了解 APK 是如何生成的, 来看一下老版构建流程图
老版构建流程图
在了解 APK 生成的过程之前,我们需要了解一下图中各个工具的作用
工具
名字 | 功能 |
---|---|
AAPT/APT2 | Android 资源打包工具 |
AIDL | 将所有的 AIDL 接口转化为 Java 接口 |
Javac(Java Compiler) | 将所有的 Java 代码编译成 Class文件 |
Dex | 将 Class 文件编译成 Dex 文件 |
Apkbuilder | 将处理后的资源和代码打包生成 APK 文件 |
Jarsigner/Apksigner | 对未签名的 APK 文件进行签名 |
Zipalign | 优化签名后的 APK,减少运行时所占用的内存 |
构建过程
Apk 的构建过程大概分为如下几步:
使用 AAPT 工具生成 R.java 文件 所有的 AIDL 接口转化为 Java 接口 将 Java 代码编译成 Class 文件 将 Class 文件编译成 Dex 文件 打包生成 APK 文件 对 APK 文件签名 优化 APK 文件
1. 使用 AAPT 工具生成 R.java 文件
AAPT(Android Asset Packaging Tool)android 资源打包工具,将资源文件(包括AndroidManifest.xml、布局文件、各种 xml 资源等)打包生成 R.java 文件,将 AndroidManifest.xml 生成二进制的 AndroidManifest.java 文件
aapt p -M AndroidManifest.xml -S output/res/ -I android.jar -J ./ -F input/out.apk
p:打包
-M:AndroidManifest.xml 文件路径
-S:res 目录路径
-A:assets 目录路径
-I:android.jar 路径,会用到的一些系统库
-J 指定生成的 R.java 的输出目录
-F 具体指定 APK 文件的输出
但是从 Android Studio 3.0 开始,google 默认开启了 AAPT2 作为资源编译的编译器,AAPT2 的出现为资源的增量编译提供了支持,aapt2 主要分两步,compile 和 link
compile
aapt2 compile -o res.apk --dir output/res/
-o:指定已编译资源的输出路径
--dir:指定包含多个资源文件的资源目录
link
aapt2 link -o input/out.apk -I tools/android.jar --manifest output/AndroidManifest.xml -A res.apk --java ./
-o:指定链接的资源 APK 的输出路径
-I:指定 android.jar 路径
--manifest:指定 AndroidManifest.xml 路径
--java :指定要在其中生成 R.java 的目录
2. 所有的 AIDL 接口转化为 Java 接口
使用 AIDL(Android Interface Denifition Language),位于 sdk\build-tools 目录下的 aidl 工具,将源码文件、aidl 文件、framework.aidl 等所有的 AIDL 文件,生成相应的 Java 文件,命令如下:
aidl -Iaidl -pAndroid/Sdk/platforms/android-29/framework.aidl -obuild aidl/com/android/vending/billing/IInAppBillingService.aidl
-I 指定 import 语句的搜索路径,注意 -I 与目录之间一定不要有空格
-p 指定系统类的 import 语句路径,如果是要用到 android.os.Bundle 系统的类,一定要设置 sdk 的 framework.aidl 路径
-o 生成 java 文件的目录,注意 -o 与目录之间一定不要有空格,而且这设置项一定要在 aidl 文件路径之前设置
3. 将 Java 代码编译成 Class 文件
使用 Javac(Java Compiler)把项目中所有的 Java 代码编译成 class 文件, 包括 Java 源文件、AAPT 生成的 R.java 文件 以及 aidl 生成的 Java 接口文件,命令如下:
javac -target 1.8 -bootclasspath platforms/android-28/android.jar -d ./java/com/testjni/*.java
4. 将 Class 文件编译成 Dex 文件
使用 DX 工具将所有的 Class 文件(包括第三方库中的 class 文件)转换成 Dex 文件(Dalvik 可执行文件,其中包括在 Android 设备上运行的字节码),该过程主要完成 Java 字节码转换成 Dalvik 字节码, 命令如下:
java -jar dx.jar --dex --ouput=classes.dex ./java/com/testjni/*.class
--dex:将 class 文件转成dex文件
--output:指定生成 dex 文件到具体位置
5. 打包生成 APK 文件
使用 Apkbuilder(主要用到的是 sdk/tools/lib/sdklib.jar 文件中的 ApkBuilderMain 类)将所有的 Dex 文件、Resource.arsc、Res 文件夹、Assets 文件夹、AndroidManifest.xml 打包生成 APK 文件(未签名)
6. 对 APK 文件签名
使用 Apksigner(Android官方针对 APK 签名及验证工具)或 Jarsigner(JDK提供针对 jar 包签名工具)对未签名的 APK 文件进行签名
ps:如果使用 Apksigner 签名需要(7. 优化 APK 文件)放到(6. 对 APK 文件签名)签名前面,为什么?请查看关于 Apksigner 和 Jarsigner 的区别,请移步到文末
7. 优化 APK 文件
使用 zipalign 对签名后的 APK 文件进行对齐处理,对齐的主要过程是将 APK 包中所有的资源文件距离文件起始偏移为 4 字节整数倍,这样通过内存映射访问 APK 文件时的速度会更快,减少其在设备上运行时所占用的内存
总结
上述打包过程都是 AndroidStudio 编译时,调用各种编译命令自动完成的, 总结一下上述打包过程:
除了 assets 和 res/raw 资源被原装不动地打包进 APK 之外,其它的资源都会被编译或者处理 除了 assets 资源之外,其它的资源都会被赋予一个资源 ID 打包工具负责编译和打包资源,编译完成之后,会生成一个 resources.arsc 文件和一个 R.java,前者保存的是一个资源索引表,后者定义了各个资源 ID 常量 应用程序配置文件 AndroidManifest.xml 同样会被编译成二进制的 xml 文件,然后再打包到 APK 里面去 应用程序在运行时通过 AssetManager 来访问资源,或通过资源 ID 来访问,或通过文件名来访问
APK 文件大概可以分为两个部分:代码和资源, 代码部分通过 Javac 将 Java 代码编译成 Class 文件, 然后通过 DX 工具将 Class 文件编译成 Dex 文件,接下来我们主要来分析一下资源的编译和打包
资源的编译和打包
在分析资源的编译和打包之前,我们需要了解一下 Android 都有哪些资源,其实 Android 资源大概分为两个部分:assets 和 res
我们使用 AAPT 对资源进行编译的时候,会采用两种模式 Deflate(压缩模式) / Stored (存储模式),而具体使用什么模式,取决于文件后缀类型,AAPT 会对以下文件后缀类型的资源采用存储模式即不会被压缩
/* these formats are already compressed, or don't compress well */
static const char* kNoCompressExt[] = {
".jpg", ".jpeg", ".png", ".gif",
".wav", ".mp2", ".mp3", ".ogg", ".aac",
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
};
通过 aapt l -v xxx.apk
或 unzip -l xxx.apk
来查看 APK 内文件使用的什么模式
1. assets 资源
assets 资源放在 assets 目录下,它里面保存一些原始的文件,可以以任何方式来进行组织,AAPT 会对指定文件后缀类型的资源进行压缩,其余的文件最终会原封不动的被打包进 APK 文件中,通过 AssetManager 来获取 asset 资源,代码如下
AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open("fileName");
2. res 资源
res 资源放在主工程的 res 目录下,这类资源一般都会在编译阶段生成一个资源ID供我们使用,res 目录包括 animator、anim、 color、drawable、layout、menu、raw、values、xml 等
上述资源文件除了 raw 类型资源,以及 drawable 文件夹下的 Bitmap 资源之外,其它的资源文件均会被编译成二进制格式的 XML 文件,生成的二进制格式的 XML 文件分别有一个字符串资源池,用来保存文件中引用到的每一个字符串
这样原来在文本格式的 XML 文件中的每一个放置字符串的地方在二进制格式的XML文件中都被替换成一个索引到字符串资源池的整数值,将整数值保存在 R.java 类中,R.java 会和其他源文件一起编译到 APK 中去
将资源编译成二进制文件,都是由 AAPT 工具来完成的,资源打包主要有以下几个流程:
解析 AndroidManifest.xml,获得应用程序的包名称,创建资源表 添加被引用资源包,被添加的资源会以一种资源 ID 的方式定义在 R.java 中 资源打包工具创建一个 AaptAssets 对象,收集当前需要编译的资源文件,收集到的资源保存在 AaptAssets 对象对象中 将上一步 AaptAssets 对象保存的资源,添加到资源表 ResourceTable 中去,用于最终生成资源描述文件 resources.arsc 编译 values 类资源,这类资源包括数组、颜色、尺寸、字符串等值 给 style、array 这类资源分配资源 ID 编译 XML 资源文件,编译的流程分为:① 解析 XML 文件 ② 赋予属性名称资源 ID ③ 解析属性值 ④ 将 XML 文件从文本格式转换为二进制格式 生成资源索引表 resources.arsc
2.1 资源 ID
AAP 工具会所有的资源都会生成一个 R.java 文件,并且每个资源都对应 R.java 中的十六进制整数变量,其实这些十六进制的整数是由三部分组成:PackageId + TypeId + ItemValue,代码所示:
public final class R {
public static final class anim {
public static final int abc_fade_in=0x7f010000;
public static final int abc_fade_in=0x7f010001;
//***
}
public static final class string {
public static final int a11y_no_data=0x7f100000;
public static final int a11y_no_permission=0x7f100001;
//***
}
}
最高字节是 Package ID 表示命名空间,标明资源的来源,Android 系统自己定义了两个 Package ID,系统资源命名空间:0x01 和 应用资源命名空间:0x7f
正因为应用资源命名空间:0x7f,我们在做插件化的时候就会出现一个问题,宿主和插件包,合并资源后资源 ID 冲突。通过上面分析要解决这个问题,就要为不同的插件设置不同的 PackageId,而宿主可以保留原来 0x7f 不变,这样就永远不会有冲突发生了
如何解决资源冲突
制定一个不用冲突的命名规范 library Module 的 build.gradle 中设置资源前缀(推荐)
android {
resourcePrefix "<前缀>"
}
2.2 资源索引(resources.arsc)
最终生成的是资源索引表 resources.arsc ,resources.arsc 是一个编译后的二进制文件, 在 AndroidStudio 打开 resources.arsc 文件,如下所示
Android 正是利用这个索引表根据资源 ID 进行资源的查找,为不同语言、不同地区、不同设备提供相对应的最佳资源。查找和通过 Resources 和 AssetManger 来完成的
在文中提到了两个工具 Apksigner 和 Jarsigner,下面一起来了解一下 Apksigner 和 Jarsigner 的区别
Apksigner 和 Jarsigner 的区别
在 Android Studio 中点击菜单 Build->Generate signed apk... 打包签名过程中,可以看到两种签名选项 V1(Jar Signature) 和 V2(Full APK Signature)
Jarsigner 是 JDK 提供的针对 JAR 包签名的通用工具 Apksigner 是 Google 官方提供的针对 Android APK 签名及验证的专用工具
在 Android 11 以上使用 V4 签名,Android 9.0 以上使用 V3 签名,Android 7.0 开始使用 V2 签名,但在 Android 7.0 以下版本, 只能用旧签名方案 V1 签名
V1 签名:
Android 7 以下使用 V1 签名,V1 签名会对 ZIP 压缩包的每个文件进行验证, 签名后还能对压缩包修改(移动/重新压缩文件),对 V1 签名的 APK/JAR 解压,在 META-INF 存放签名文件(MANIFEST.MF, CERT.SF, CERT.RSA), 其中 MANIFEST.MF 文件保存所有文件的 SHA1 指纹(除了 META-INF 文件), 由此可知: V1 签名是对压缩包中单个文件签名验证
V2 签名:
Android 7 开始增加了 V2 签名,V2 签名会对 ZIP 压缩包的整个文件验证, 签名后不能修改压缩包(包括 zipalign), 对 V2 签名的 APK 解压, 没有发现签名文件, 重新压缩后 V2 签名就失效, 由此可知: V2 签名是对整个 APK 签名验证
V3 签名:
Android 9 增加了 V3 签名,V3 签名在 V2 的基础上,仍然采用检查整个压缩包的校验方式,支持 APK 密钥轮替,这使应用能够在 APK 更新过程中更改其签名密钥
v3 签名新增的新块(attr) 会记录我们之前的签名信息以及新的签名信息,支持 APK 密钥轮替方案,来做签名的替换和升级。这意味着,只要旧签名证书在手,我们就可以通过它在新的 APK 文件中,更改签名。
需要注意的是:对于覆盖安装的情况,签名校验只支持升级,而不支持降级
V4 签名:
在 Android 11 之前,建议不要使用 APK 密钥轮替,在 Android 11 之后增加了 V4 签名,V4 签名将签名存储在单独的 <apk name>.apk.idsig 文件中。v4 签名需要 v2 或 v3 签名作为补充。
关于签名更多内容,会在后续文章中介绍。
总结
V1 签名是对压缩包中单个文件签名验证 V2 签名是对整个 APK 签名验证 zipalign 可以在 V1 签名后执行 zipalign 不能在 V2 签名后执行,只能在 V2 签名之前执行 V2 签名更安全(不能修改压缩包) V2 签名验证时间更短(不需要解压验证), 因而安装速度加快 apksigner 工具默认同时使用 V1 和 V2 签名, 以兼容 Android 7.0 以下版本 Android 7 以下使用 V1 签名,Android 7 开始增加了 V2 签名,Android 9 增加了 V3 签名,Android 11 之后增加了 V4 签名 V3 签名 和 V4 签名 目前只能在 Google Play 上使用
参考文献
https://developer.android.com/studio/build/index.html?hl=zh-cn#build-process https://developer.android.com/studio/build/index.html?hl=zh-cn#build-process https://developer.android.com/studio/command-line/aapt2?hl=zh-cn
推荐阅读:
最后推荐我一直在更新维护的项目和网站:
全新系列视频:现代 Android 开发 (MAD) 技巧系列教程
https://madskills.hi-dhl.com最新的 AndroidX Jetpack 相关组件的实战项目 以及 原理分析的文章
https://github.com/hi-dhl/AndroidX-Jetpack-PracticeLeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析
剑指 offer:https://offer.hi-dhl.com
LeetCode:https://leetcode.hi-dhl.com最新 Android 10 源码分析系列文章
https://github.com/hi-dhl/Android10-Source-Analysis一系列国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的分析
https://github.com/hi-dhl/Technical-Article-Translation「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址
https://site.51git.cn
致力于分享一系列最新技术原创文章
长按二维码即可关注
我知道你在看哟